#region Using Statements
using System;
using System.Linq;
using System.Diagnostics;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.GamerServices;
using PFAGame.NetworkingGame;
#endregion

namespace PFAGame
{
    class NetworkSessionComponent : GameComponent
    {
        #region Public const

        public const float NetworkUpdate = 0.11f;

        public const int MaxGamers = 4;
        public const int MaxLocalGamers = 2;

        public enum PacketTypes
        {
            PlayerInit,
            PlayerData,
            WorldSetup,
            WorldData,
            PlayerDeath,
            GameEnded,
            MobData,
            ProjectileData,
            LootData,
            NewAttack,
        };

        #endregion

        #region Fields

        ScreenManager screenManager;
        NetworkSession networkSession;
        IMessageDisplay messageDisplay;

        bool notifyWhenPlayersJoinOrLeave;

        string sessionEndMessage;

        private PacketReader packetReader = new PacketReader();
        private PacketWriter packetWriter = new PacketWriter();

        private float doUpdate = 0.0f;

        private bool isSeedInit = false;
        private List<NetworkGamer> playerWaitingForSeed = new List<NetworkGamer>();

        private List<Specialiste.ShootData> shootsWaiting = new List<Specialiste.ShootData>();

        #endregion

        #region Initialization

        NetworkSessionComponent(ScreenManager screenManager,
                                NetworkSession networkSession)
            : base(screenManager.Game)
        {
            this.screenManager = screenManager;
            this.networkSession = networkSession;

            // Hook up our session event handlers.
            networkSession.GameStarted += GameStarted;
            networkSession.GamerJoined += GamerJoined;
            networkSession.GamerLeft += GamerLeft;
            networkSession.SessionEnded += NetworkSessionEnded;
            networkSession.HostChanged += HostChanged;
            Map.seedGenerated += new Map.SeedGeneratedEventHandler(Map_seedGenerated);
            Specialiste.shootEvent += new Specialiste.ShootEventHandler(Specialiste_shootEvent);
            //LobbyScreen.NotifyJoined += new LobbyScreen.NotifyJoinedEventHandler(LobbyScreen_NotifyJoined);
        }

        /// <summary>
        /// Creates a new NetworkSessionComponent.
        /// </summary>
        public static void Create(ScreenManager screenManager,
                                  NetworkSession networkSession)
        {
            Game game = screenManager.Game;

            networkSession.AllowHostMigration = false;
            networkSession.AllowJoinInProgress = true;
            // Register this network session as a service.
            game.Services.AddService(typeof(NetworkSession), networkSession);

            // Create a NetworkSessionComponent, and add it to the Game.
            game.Components.Add(new NetworkSessionComponent(screenManager,
                                                            networkSession));
        }


        public override void Initialize()
        {
            base.Initialize();

            messageDisplay = (IMessageDisplay) Game.Services.GetService(
                                                              typeof(IMessageDisplay));

            if (messageDisplay != null)
                notifyWhenPlayersJoinOrLeave = true;
        }


        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                // Remove the NetworkSessionComponent.
                Game.Components.Remove(this);

                // Remove the NetworkSession service.
                Game.Services.RemoveService(typeof(NetworkSession));

                // Dispose the NetworkSession.
                if (networkSession != null)
                {
                    networkSession.Dispose();
                    networkSession = null;
                }
            }

            base.Dispose(disposing);
        }

        #endregion

        #region Update

        public override void Update(GameTime gameTime)
        {
            if (networkSession == null)
                return;
            try
            {
                doUpdate += (float) gameTime.ElapsedGameTime.Ticks / 10000000;
                /* WRITE */
                if (networkSession.SessionState == NetworkSessionState.Playing && doUpdate >= NetworkUpdate)
                {
                    if (this.networkSession.IsHost)
                        this.UpdateNetworkGame();
                    else
                        this.SendShootWaiting();
                    this.UpdatePlayerData();
                }

                networkSession.Update();

                // Has the session ended?
                if (networkSession.SessionState == NetworkSessionState.Ended)
                {
                    LeaveSession();
                }
                /* READ */
                else if (networkSession.SessionState == NetworkSessionState.Playing && doUpdate >= NetworkUpdate)
                {
                    foreach (LocalNetworkGamer gamer in networkSession.LocalGamers)
                    {
                        ReadNetworkGame(gamer);
                    }
                    doUpdate = 0.0f;
                }
            }

            catch (Exception exception)
            {
                Debug.WriteLine("NetworkSession.Update threw " + exception);

                sessionEndMessage = Resources.ErrorNetwork;

                LeaveSession();
            }
        }

        #endregion

        #region Private Method

        private void SendShootWaiting()
        {
            foreach (Specialiste.ShootData shoot in this.shootsWaiting)
            {
                this.packetWriter.Write((int) PacketTypes.NewAttack);
                shoot.Serialize(packetWriter);
                this.networkSession.LocalGamers[0].SendData(packetWriter, SendDataOptions.ReliableInOrder, this.networkSession.Host);
            }
            this.shootsWaiting.Clear();
        }

        private void ReadNetworkGame(LocalNetworkGamer gamer)
        {
            NetworkGamer sender;

            while (gamer.IsDataAvailable)
            {
                gamer.ReceiveData(packetReader, out sender);

                if (sender.IsLocal)
                    continue;

                PacketTypes packetType = (PacketTypes) packetReader.ReadInt32();
                PlayerNet playerData = sender.Tag as PlayerNet;

                switch (packetType)
                {
                    case PacketTypes.WorldSetup:
                        playerData.InitializeWorld(packetReader);
                        break;
                    case PacketTypes.WorldData:
                        playerData.UpdateWorld(packetReader);
                        break;
                    //case PacketTypes.PlayerInit:
                    //    playerData.Initialize(packetReader, gamer.Gamertag);
                    //    break;
                    case PacketTypes.PlayerData:
                        playerData.Deserialize(packetReader, sender.Gamertag);
                        break;
                    case PacketTypes.NewAttack:
                        playerData.DeserializeShootData(packetReader);
                        break;
                    case PacketTypes.LootData:
                        playerData.UpdateLoot(packetReader);
                        break;
                    case PacketTypes.MobData:
                        playerData.UpdateMob(packetReader);
                        break;
                    case PacketTypes.ProjectileData:
                        playerData.UpdateProjectile(packetReader);
                        break;
                }
            }
        }

        private void UpdateNetworkGame()
        {
            LocalNetworkGamer localGamer = this.networkSession.Host as LocalNetworkGamer;

            if (GameplayScreen.Singleton.MobList != null)
            {
                packetWriter.Write((int) PacketTypes.MobData);
                packetWriter.Write(GameplayScreen.Singleton.MobList.Count);
                foreach (AMob mob in GameplayScreen.Singleton.MobList)
                {
                    MobNet.Serialize(mob, packetWriter);
                }
                localGamer.SendData(packetWriter, SendDataOptions.None);
            }
            if (GameplayScreen.Singleton.LootList != null)
            {
                packetWriter.Write((int) PacketTypes.LootData);
                packetWriter.Write(GameplayScreen.Singleton.LootList.Count);
                foreach (ALoot loot in GameplayScreen.Singleton.LootList)
                {
                    LootNet.Serialize(loot, packetWriter);
                }
                localGamer.SendData(packetWriter, SendDataOptions.None);
            }
            if (GameplayScreen.Singleton.ProjectileList != null)
            {
                packetWriter.Write((int) PacketTypes.ProjectileData);
                packetWriter.Write(GameplayScreen.Singleton.ProjectileList.Count);
                foreach (AProjectile proj in GameplayScreen.Singleton.ProjectileList)
                {
                    ProjNet.Serialize(proj, packetWriter);
                }
                localGamer.SendData(packetWriter, SendDataOptions.None);
            }
        }

        private void UpdatePlayerData()
        {
            foreach (APlayer localPlayer in GameplayScreen.Singleton.Players)
            {
                packetWriter.Write((int) PacketTypes.PlayerData);
                PlayerNet.Serialize(localPlayer, packetWriter);
                this.networkSession.LocalGamers[0].SendData(packetWriter, SendDataOptions.InOrder);
            }
        }

        private void sendSeedToPlayer(NetworkGamer gamer)
        {
            LocalNetworkGamer localGamer = this.networkSession.Host as LocalNetworkGamer;

            if (isSeedInit)
            {
                packetWriter.Write((int) PacketTypes.WorldSetup);
                packetWriter.Write(GameplayScreen.Singleton.MapP.Seed);
                packetWriter.Write(GameplayScreen.Singleton.CurrentLevel);
                packetWriter.Write((int) GameplayScreen.Singleton.gameMode);
                localGamer.SendData(packetWriter, SendDataOptions.ReliableInOrder, gamer);
            }
            else
                this.playerWaitingForSeed.Add(gamer);
        }

        #endregion

        #region Event Handlers

        //private void LobbyScreen_NotifyJoined(object sender, e_class classChoose)
        //{
        //    this.packetWriter.Write((int) PacketTypes.PlayerInit);
        //    this.packetWriter.Write((int) classChoose);
        //    this.networkSession.LocalGamers[0].SendData(packetWriter, SendDataOptions.ReliableInOrder);
        //}

        void Specialiste_shootEvent(object sender, Specialiste.ShootData shootData)
        {
            shootsWaiting.Add(shootData);
        }

        private void Map_seedGenerated(object sender, int seed)
        {
            isSeedInit = true;
            foreach (NetworkGamer gamer in playerWaitingForSeed)
            {
                sendSeedToPlayer(gamer);
            }
            playerWaitingForSeed.Clear();
        }

        private void GameStarted(object sender, GameStartedEventArgs e)
        {
            if ((this.networkSession != null) && this.networkSession.IsHost)
            {
                foreach (NetworkGamer gamer in this.networkSession.AllGamers)
                {
                    if (gamer.IsLocal)
                        continue;

                    PlayerNet playerNet = gamer.Tag as PlayerNet;

                    sendSeedToPlayer(gamer);
                }
            }
        }

        private void HostChanged(object sender, HostChangedEventArgs e)
        {
            messageDisplay.ShowMessage(Resources.MessageNewHost,
                e.NewHost.Gamertag);
        }

        private void GamerJoined(object sender, GamerJoinedEventArgs e)
        {
            if (notifyWhenPlayersJoinOrLeave)
            {
                messageDisplay.ShowMessage(Resources.MessageGamerJoined,
                                           e.Gamer.Gamertag);
            }

            if (!e.Gamer.IsLocal)
            {
                e.Gamer.Tag = new PlayerNet();

                if (networkSession.IsHost)
                {
                    sendSeedToPlayer(e.Gamer);
                }
            }
        }

        private void GamerLeft(object sender, GamerLeftEventArgs e)
        {
            if (notifyWhenPlayersJoinOrLeave)
            {
                messageDisplay.ShowMessage(Resources.MessageGamerLeft,
                                           e.Gamer.Gamertag);
            }

            if (!e.Gamer.IsLocal)
            {
                PlayerNet playerNet = e.Gamer.Tag as PlayerNet;
                List<APlayer> tmpPlayerDestroy = new List<APlayer>();

                foreach (APlayer player in GameplayScreen.Singleton.DicoPlayer.Values)
                    if (player == playerNet.player)
                        tmpPlayerDestroy.Add(player);
                foreach (APlayer player in tmpPlayerDestroy)
                    GameplayScreen.Singleton.DicoPlayer.Remove(player.Id);
            }
        }


        void NetworkSessionEnded(object sender, NetworkSessionEndedEventArgs e)
        {
            switch (e.EndReason)
            {
                case NetworkSessionEndReason.ClientSignedOut:
                    sessionEndMessage = null;
                    break;

                case NetworkSessionEndReason.HostEndedSession:
                    sessionEndMessage = Resources.ErrorHostEndedSession;
                    break;

                case NetworkSessionEndReason.RemovedByHost:
                    sessionEndMessage = Resources.ErrorRemovedByHost;
                    break;

                case NetworkSessionEndReason.Disconnected:
                default:
                    sessionEndMessage = Resources.ErrorDisconnected;
                    break;
            }

            notifyWhenPlayersJoinOrLeave = false;
        }


        public static void InviteAccepted(ScreenManager screenManager,
                                          InviteAcceptedEventArgs e)
        {
            NetworkSessionComponent self = FindSessionComponent(screenManager.Game);

            if (self != null)
                self.Dispose();

            try
            {
                IEnumerable<SignedInGamer> localGamers =
                    ChooseGamers(NetworkSessionType.PlayerMatch, e.Gamer.PlayerIndex);

                IAsyncResult asyncResult = NetworkSession.BeginJoinInvited(localGamers,
                                                                           null, null);

                NetworkBusyScreen busyScreen = new NetworkBusyScreen(asyncResult);

                busyScreen.OperationCompleted += JoinInvitedOperationCompleted;

                LoadingScreen.Load(screenManager, false, null, new BackgroundScreen(),
                                                               busyScreen);
            }
            catch (Exception exception)
            {
                NetworkErrorScreen errorScreen = new NetworkErrorScreen(exception);

                LoadingScreen.Load(screenManager, false, null, new BackgroundScreen(),
                                                               new MainMenuScreen(),
                                                               errorScreen);
            }
        }

        static void JoinInvitedOperationCompleted(object sender,
                                                  OperationCompletedEventArgs e)
        {
            ScreenManager screenManager = ScreenManager.Singleton;

            try
            {
                NetworkSession networkSession =
                                        NetworkSession.EndJoinInvited(e.AsyncResult);

                NetworkSessionComponent.Create(screenManager, networkSession);

                screenManager.AddScreen(new LobbyScreen(networkSession), null);
            }
            catch (Exception exception)
            {
                screenManager.AddScreen(new MainMenuScreen(), null);
                screenManager.AddScreen(new NetworkErrorScreen(exception), null);
            }
        }


        #endregion

        #region Methods

        public static bool IsOnlineSessionType(NetworkSessionType sessionType)
        {
            switch (sessionType)
            {
                case NetworkSessionType.Local:
                case NetworkSessionType.SystemLink:
                    return false;

                case NetworkSessionType.PlayerMatch:
                case NetworkSessionType.Ranked:
                    return true;

                default:
                    throw new NotSupportedException();
            }
        }

        public static IEnumerable<SignedInGamer> ChooseGamers(
                                                        NetworkSessionType sessionType,
                                                        PlayerIndex playerIndex)
        {
            List<SignedInGamer> gamers = new List<SignedInGamer>();

            SignedInGamer primaryGamer = Gamer.SignedInGamers[playerIndex];

            if (primaryGamer == null)
                throw new GamerPrivilegeException();

            gamers.Add(primaryGamer);

            foreach (SignedInGamer gamer in Gamer.SignedInGamers)
            {
                if (gamers.Count >= MaxLocalGamers)
                    break;

                if (gamer == primaryGamer)
                    continue;

                if (IsOnlineSessionType(sessionType))
                {
                    if (!gamer.IsSignedInToLive)
                        continue;

                    if (!gamer.Privileges.AllowOnlineSessions)
                        continue;
                }

                if (primaryGamer.IsGuest && !gamer.IsGuest && gamers[0] == primaryGamer)
                {
                    gamers.Insert(0, gamer);
                }
                else
                {
                    gamers.Add(gamer);
                }
            }

            return gamers;
        }


        public static void LeaveSession(ScreenManager screenManager,
                                        PlayerIndex playerIndex,
                                        bool isLeavingFromPause)
        {
            NetworkSessionComponent self = FindSessionComponent(screenManager.Game);

            if (self != null)
            {
                if (isLeavingFromPause == true)
                {
                    self.LeaveSession();
                    return;
                }
                // Display a message box to confirm the user really wants to leave.
                string message;

                if (self.networkSession.IsHost)
                    message = Resources.ConfirmEndSession;
                else
                    message = Resources.ConfirmLeaveSession;

                MessageBoxScreen confirmMessageBox = new MessageBoxScreen(message);

                // Hook the messge box ok event to actually leave the session.
                confirmMessageBox.Accepted += delegate
                {
                    self.LeaveSession();
                };
                
                screenManager.AddScreen(confirmMessageBox, playerIndex);
            }
        }


        void LeaveSession()
        {
            // Destroy this NetworkSessionComponent.
            Dispose();

            MessageBoxScreen messageBox;

            if (!string.IsNullOrEmpty(sessionEndMessage))
                messageBox = new MessageBoxScreen(sessionEndMessage, false);
            else
                messageBox = null;

            GameScreen[] screens = screenManager.GetScreens();

            // Look for the MainMenuScreen.
            for (int i = 0; i < screens.Length; i++)
            {
                if (screens[i] is MainMenuScreen)
                {
                    // If we found one, pop everything since then to return back to it.
                    for (int j = i + 1; j < screens.Length; j++)
                        screens[j].ExitScreen();

                    // Display the why-did-the-session-end message box.
                    if (messageBox != null)
                        screenManager.AddScreen(messageBox, null);

                    return;
                }
            }

            LoadingScreen.Load(screenManager, false, null, new BackgroundScreen(),
                                                           new MainMenuScreen(),
                                                           messageBox);
        }


        static NetworkSessionComponent FindSessionComponent(Game game)
        {
            return game.Components.OfType<NetworkSessionComponent>().FirstOrDefault();
        }


        #endregion
    }
}
